// Copyright 2022 Huawei Cloud Computing Technology Co., Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import JMuxer from 'jmuxer';
import work from 'webworkify-webpack';
import AESGCMCrypto from './AESGCMCrypto';
import AudioPlayer from './AudioPlayer';
import AutoRotation from './AutoRotation';
import CanvasPlayer from './CanvasPlayer';
import Subscribe from './common/Subscribe';
import CANVAS_WHITE_LIST from './config/canvasWhiteList';
import PROTOCOL_CONFIG from './config/protocolConfig';
import delayAnalysis from './DelayAnalysis';
import FrameParser from './FrameParser';
import Logger from './Logger';
import TouchHandler from './TouchHandler';
import Util from './Util';
import SocketWorker from './worker/SocketWorker';
import KeyboardInput from "./KeyboardInput";
import DirectionHandler from './DirectionHandler';
import DeviceHardwareHandler from './DeviceHardwareHandler';
import {
    PACKAGE_HEADER_LENGTH,
    MEDIA_MSG_HEADER_COUNT,
    DEFAULT_DEFINITION,
    FRAME_TYPE_MAP
} from './config/commonConfig';
import FullScreen from './Fullscreen';

/*global __IS_DEBUG__*/
if (__IS_DEBUG__) {
    window.delayAnalysis = delayAnalysis;
}

const CAE_STREAM_DELIMITER_MAGICWORD = 0x5A5A;
const K_UNIT = 1000;
const K_BIT_UNIT = 8000;
const DEFAULT_ORIENTATION = 'PORTRAIT';
const APP_STATE_FROM_CLIENT = {
    'connecting': {
        state: 256,
        message: 'Connecting'
    },
    'connected': {
        state: 512,
        message: 'Connect success'
    },
    'unreachable': {
        state: 769,
        message: 'Server unreachable',
        tip: '连接服务器失败，请稍后重试'
    },
    'reconnecting': {
        state: 2816,
        message: 'Reconnecting'
    },
    'exit': {
        state: 5888,
        message: 'Cloud Phone exit',
        tip: '已退出'
    }
};
const APP_STATE_ERROR_CODE_TIP = {
    5888: '已退出',
    769: '连接服务器失败，请稍后重试',
    2308: '启动失败',
    4353: '启动失败',
    770: '资源正在使用中，请稍后',
    65535: '与服务器连接出现异常',
    1537: '认证失败',
    1538: '认证失败',
    1539: '认证失败',
    1540: '认证失败',
    1541: '认证失败',
    1542: '认证失败',
    1543: '登录信息失效，请重新登录',
    3584: '试玩时间已到',
    2560: '与服务器连接出现异常',
    3840: '由于您长时间未操作游戏，服务断开',
    4096: '切换后台超时'
};
const DEFAULT_VALUME_VALUE = 50;
const WEBSOCKET_READY_STATE = {
    CONNECTING: 0,
    OPEN: 1,
    CLOSING: 2,
    CLOSED: 3
};
const WORKER_STATE = {
    CHECKING: 0,
    LIVE: 1
};

class AppController {
    constructor(options) {
        this.options = {volume: DEFAULT_VALUME_VALUE, ...options};
        this.util = new Util();
        this.render = true;
        this.player = null;
        this.playerContainerId = undefined;
        this.videoEleId = 'phoenixVideo';
        this.canvasEleId = 'phoenixCanvas';
        this.subscribe = new Subscribe([
            'netStateChange',
            'appStateChange',
            'audioStateChange',
            'cloudAppData'
        ]);
        this.cameraMsgHeader = null;
        this.microPhoneMsgHeader = null;
        this.cameraMsgBody = null;
        this.microPhoneMsgBody = null;
        this.sensorMsgHeader = null;
        this.cameraMsgType = PROTOCOL_CONFIG.CAMERA_MESSAGE_TYPE;
        this.microPhoneMsgType = PROTOCOL_CONFIG.MICROPHONE_MESSAGE_TYPE;
        this.sensorMsgType = PROTOCOL_CONFIG.SENSOR_MESSAGE_TYPE;
        // WebSocket variable
        this.useSocketWorker = AppController.isSupportURL();
        this.wsState = undefined;
        this.sessionId = this.options.sessionId || this.generateGUID();
        this.reconnection = {
            can: true,
            maxTimes: this.options.reconnectTimes,
            count: 0,
            timerDelay: 1000,
            timerHander: null,
            reconnecting: false, // 用于标识已进入重连流程，避免page visibility、page show处理时判断是否要主动触发重连
            trigger: '' // 标识什么事件触发了重连，visibilityChange/pageshow/socketCloseEvent
        };
        // 网络时延/码率
        this.networkInfo = {
            heartBeatSendTimes: [],
            sendTimesMaxCount: 50,
            delay: 0,
            bitRate: 0,
            kbitCount: 0,
            lastRefreshTime: 0
        };
        this.lastReceivingTime = null;
        this.heartBeatInterval = 1000;
        this.linkOverTime = 4000;

        this.frameParser = new FrameParser(this.options.supportAudio);
        this.autoRotation = null;
        this.curResolution = {...PROTOCOL_CONFIG.DEFAULT_RESOLUTION};
        this.nextResolution = {...PROTOCOL_CONFIG.DEFAULT_RESOLUTION};

        this.createSocket();
        this.socketWorkerState = {
            worker: null,
            socket: null
        };
        this.appState = null;
        this.socketHasOpenned = false; // 标识socket是否已open过，用于重连时判断发送connect还是reconnect cmd

        // 初始化全屏类
        this.fullscreen = new FullScreen({
            containerId: this.options.containerId,
            autoRotate: this.options.autoRotate,
            isMobile: this.options.isMobile
        });
        this.fullscreen.init();
        this.triggerFullscreen();
    }

    getVolume() {
        return this.options.volume;
    }

    setVolume(value) {
        // audioPlayer 不存在时通过 options 的 volume 属性暂存 volume 的值
        this.options.volume = value;
        if (this.audioPlayer) {
            this.audioPlayer.setVolume(value);
        }
    }

    createSocket() {
        if (this.useSocketWorker) {
            this.__createSocketWorker();
        } else {
            this.__createSocketWithoutWorker();
        }
    }

    __createSocketWorker() {
    
        /* javascript-obfuscator:disable */
        this.socketWorker = work(require.resolve('./worker/SocketWorker.js'));

        /* javascript-obfuscator:enable */
        this.socketWorker.addEventListener('message', evt => {
            const objData = evt.data;
            switch (objData.cmd) {
                case 'openRsp':
                    this.onOpenSocket(objData.state);
                    break;
                case 'errorRsp':
                    this.onError();
                    break;
                case 'recvRsp':
                    this.onMessage(objData.buf, objData.time);
                    break;
                case 'heartbeatRsp':
                    this.recordHeartbeat(objData.time);
                    break;
                case 'closeRsp':
                    if ((this.reconnection.reconnecting && this.reconnection.trigger === 'socketCloseEvent') || !this.reconnection.reconnecting) {
                        this.onClose(objData.state, 'socketCloseEvent');
                    }

                    break;
                case 'socketStateRsp':
                    this.updateSocketWorkerState(WORKER_STATE.LIVE, objData.state);
                    break;
                default:
                    Logger.debug('Unknown command from socket worker.');
            }
        });
        this.socketWorker.addEventListener('error', () => {
            Logger.debug('Socket worker error.');
        });

        this.initSocket = connectURI => {
            this.socketWorker.postMessage({
                cmd: 'initReq',
                options: {
                    protocol: 'wss',
                    connectURI,
                    needHeatBeat: this.options.needHeatBeat
                }
            });
        };

        this.send = data => {
            this.socketWorker.postMessage({cmd: 'sendReq', data: data});
        };

        this.startHeartbeat = () => {
            this.socketWorker.postMessage({cmd: 'startHeartbeatReq'});
        };

        this.stopHeartbeat = () => {
            this.socketWorker.postMessage({cmd: 'stopHeartbeatReq'});
        };

        this.closeSocket = () => {
            this.socketWorker.postMessage({cmd: 'closeReq'});
        };

        this.checkSocketWorkerState = () => {
            this.updateSocketWorkerState(WORKER_STATE.CHECKING, null);
            this.socketWorker.postMessage({cmd: 'socketStateReq'});
        };
    }

    __createSocketWithoutWorker () {
        this.socket = new SocketWorker();
        this.initSocket = connectURI => {
            this.socket.init({
                protocol: 'wss',
                connectURI,
                needHeatBeat: this.options.needHeatBeat
            });
        };

        // playEvent使用时，使用self
        this.send = data => {
            this.socket.send(data);
        };

        this.startHeartbeat = () => {
            this.socket.startHeartbeat();
        };

        this.stopHeartbeat = () => {
            this.socket.stopHeartbeat();
        };

        this.closeSocket = () => {
            this.socket.destroy();
        };

        this.socket.onOpen = this.onOpenSocket.bind(this);
        this.socket.onError = this.onError.bind(this);
        this.socket.onMessage = data => {
            this.onMessage(data, Date.now());
        };

        this.socket.onClose = this.onClose.bind(this);
        this.socket.onHeartbeatSended = this.recordHeartbeat.bind(this);
    }

    onOpenSocket(state) {
        this.socketHasOpenned = true;
        this.wsState = state;
        // page visibility场景检查socket状态后延迟处理，此处刷新socket状态，避免在延迟时间里close触发重连后，page visibility再次重连。
        this.updateSocketWorkerState(WORKER_STATE.LIVE, state);
        this.reconnection.can = true;
        this.reconnection.count = 0;
        this.reconnection.reconnecting = false;

        // 发送启动命令获取音视频数据
        switch (this._action) {
            case 'reconnect':
                this._reconnect();
                break;
            default:
                this.appState = APP_STATE_FROM_CLIENT.connected;
                this.subscribe.trigger('appStateChange', {...this.appState});
                this.startCloudPhone();
                this.listenPageVisibility();
                this.listenPageShow();

                this.deviceHardwareHandler = new DeviceHardwareHandler({
                    ...this.options,
                    sendMediaData: this.sendMediaData.bind(this),
                    cameraMsgType: this.cameraMsgType,
                    microPhoneMsgType: this.microPhoneMsgType,
                    sensorMsgType: this.sensorMsgType,
                    setMsgHeader: this.setMsgHeader.bind(this),
                    send: this.send.bind(this)
                });
        }
    }

    initPlyerAndStart() {
        this.initPlayer();

        if (this.player) {
            this.keyboardInput = new KeyboardInput(this.options.containerId, this.send);
            this.keyboardInput.init();
            this.touchHandler = new TouchHandler({
                player: this.player,
                isMobile: this.options.isMobile,
                sendHandler: this.send,
                isDebug: this.options.isDebug,
                autoRotate: this.options.autoRotate,
                inputId: this.keyboardInput && this.keyboardInput.inputId || ''
            });
            this.touchHandler.start();

            if(this.touchHandler.displayBox.width && this.touchHandler.displayBox.height) {
                const ctrlEle = document.getElementById('controlBtn');
                const containerEle = document.getElementById('container');
                const left = (containerEle.offsetWidth - this.touchHandler.displayBox.width)/2 + ctrlEle.clientWidth/2;
                const top = (containerEle.offsetHeight - this.touchHandler.displayBox.height)/2 + ctrlEle.clientHeight * 2.5;
                ctrlEle.style.top = `${top}px`;
                ctrlEle.style.left = `${left}px`;
                ctrlEle.style.userSelect = 'none';
            }
            this.directionHandler = new DirectionHandler({
                containerId: this.options.containerId,
                isMobile: this.options.isMobile,
                playerContainerId: this.playerContainerId,
                touchHandler: this.touchHandler
            });
            this.directionHandler.init();
            this.listenPlayerSizeChange();

            this.autoRotation = new AutoRotation(this.options.containerId,
                DEFAULT_ORIENTATION, this.options.isMobile,rotateDegrees => {
                    // 旋转后更新触控，并根据旋转角度判断使用云机键盘还是真机键盘
                    this.touchHandler.resize();
                });
            this.autoRotation.init();

            // start success后再初始化编码器
            if (!this.isMSE) {
                const frameType = this.recMediaConfig.frame_type.toUpperCase();
                this.avc.loadjs(frameType);
            }

            this.updateResolutionAndTouch();
        }
    }

    triggerSubscribe(event, data) {
        this.subscribe.trigger(event.name, data);
    }

    listenPlayerSizeChange() {
        if (this.isMSE) {
            // 视频元信息加载完成后，主动触发 resize 事件更新相关坐标位置
            this.loadedmetadataCallback = () => {
                this.touchHandler.resize();
                // 使用完毕移除事件监听
                this.player.removeEventListener('loadedmetadata', this.loadedmetadataCallback);
                this.loadedmetadataCallback = null;
            };

            this.player.addEventListener('loadedmetadata', this.loadedmetadataCallback);

            // video 大小变化时需要持续更新坐标位置
            this.videoResizeCallback = () => {
                this.touchHandler.resize();
            };

            this.player.addEventListener('resize', this.videoResizeCallback);
        } else {
            // 监听 canvas 大小变化
            const MutationObserver = window.MutationObserver;
            this.canvasObserver = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    if (mutation.attributeName === 'width' || mutation.attributeName === 'height') {
                        this.touchHandler.resize();
                    }
                });
            });
            this.canvasObserver.observe(this.player, {attributes: true});
        }
    }

    onMessage(data, time) {
        this.lastReceivingTime = time;

        let videoNum = 0;
        let beforeParseTime = 0;
        /*global __IS_DEBUG__*/
        if (__IS_DEBUG__) {
            videoNum = this.frameParser.getPackageCacheNum('Video');
            beforeParseTime = Date.now();
        }

        this.frameParser.readPackage(data);
        /*global __IS_DEBUG__*/
        if (__IS_DEBUG__ && this.frameParser.getPackageCacheNum('Video') - videoNum > 0) {
            let traceId = window.delayAnalysis.allocTraceId();
            window.delayAnalysis.record(['receive', 'end', traceId], null, time);
            window.delayAnalysis.record(['parse', 'start', traceId], null, beforeParseTime);
            window.delayAnalysis.record(['parse', 'end', traceId]);
            window.delayAnalysis.cacheTraceId('receive', traceId);
        }

        let videoFrame = this.frameParser.shiftPackage('Video');
        if (videoFrame && this.playing) {
            this.play(videoFrame);
        }

        // 计算网络时延
        const tempNetworkInfo = this.networkInfo;
        tempNetworkInfo.kbitCount += (data || []).byteLength / K_UNIT;

        if (this.frameParser.shiftPackage('HeartBeat')) {
            this.__calcNetworkState();
        }

        // 增加播放状态条件限制，避免destroy后播放报错
        if (this.options.supportAudio && this.playing && this.audioPlayer) {
            const frame = this.frameParser.shiftPackage('Audio');
            frame && this.audioPlayer.feed(frame);
        }

        const cmdControlPkg = this.frameParser.shiftPackage('CmdControl');
        this.processCmdControlResp(cmdControlPkg);

        const phoneControlPkg = this.frameParser.shiftPackage('PhoneControl');
        this.processPhoneControlResp(phoneControlPkg);

        const cameraPkg = this.frameParser.shiftPackage('Camera');
        this.cameraProcessResp(cameraPkg);

        const microPhonePkg = this.frameParser.shiftPackage('MicroPhone');
        this.microPhoneProcessResp(microPhonePkg);

        const sensorPkg = this.frameParser.shiftPackage('Sensor');
        this.sensorProcessResp(sensorPkg);

        // 若启动竖屏，CAE不发送旋转msg
        const orientationPkg = this.frameParser.shiftPackage('Orientation');
        if (orientationPkg) {
            let pkgBody = orientationPkg & 0xFF;
            const orientation = PROTOCOL_CONFIG.ORIENTATION[pkgBody];
            Logger.debug('Orientation change：' + orientation);
            this.pkgBody = pkgBody;
            this.orientation = orientation;
            this.updateResolutionAndTouch();
        }

        const channelDataPkg = this.frameParser.shiftPackage('Channel');
        if (channelDataPkg) {
            this.processChannelData(channelDataPkg)
        }

        const keyboardInputPkg = this.frameParser.shiftPackage('KeyboardInput');
        if (keyboardInputPkg) {
            this.processKeyboardInput(keyboardInputPkg);
        }
    }

    updateResolutionAndTouch() {
        if (this.player && this.orientation) {
            // resize之前更新canvas大小
            if (this.options.isMobile) {
                this.directionHandler.updateMobileCanvasSize(this.orientation, this.isMSE);
            } else {
                this.directionHandler.updateCanvasSize(this.orientation, this.isMSE);
            }
            this.touchHandler.updateOrientation(this.orientation);
            this.autoRotation && this.autoRotation.updateOrientation(this.orientation, this.touchHandler.displayBox, this.directionHandler);
        }
    }

    __calcNetworkState() {
        if (this.networkInfo.heartBeatSendTimes.length > 0) {
            this.networkInfo.delay = this.lastReceivingTime - this.networkInfo.heartBeatSendTimes.shift();
        }

        if (this.lastReceivingTime !== this.networkInfo.lastRefreshTime) {
            this.networkInfo.bitRate = Math.round(this.networkInfo.kbitCount * K_BIT_UNIT / (this.lastReceivingTime - this.networkInfo.lastRefreshTime));
            this.networkInfo.lastRefreshTime = this.lastReceivingTime;
            this.networkInfo.kbitCount = 0;
        }

        this.subscribe.trigger('netStateChange', {
            delay: this.networkInfo.delay,
            bitrate: this.networkInfo.bitRate
        });
    }

    onError() {
        this.appState = APP_STATE_FROM_CLIENT.unreachable;
        this.subscribe.trigger('appStateChange', {...this.appState});
    }

    updateResolution() {
        if (this.curResolution.width === this.nextResolution.width && this.curResolution.height === this.nextResolution.height) {
            return;
        }

        this.touchHandler.updateResolution(this.nextResolution.width, this.nextResolution.height);
        this.curResolution = this.nextResolution;
    }

    microPhoneProcessResp(pkg) {
        if (!pkg) {
            return;
        }

        let buf = new Uint8Array(pkg);
        this.microPhoneMsgHeader = this.deviceHardwareHandler.getMsgHeader(buf);
        if (buf.length > MEDIA_MSG_HEADER_COUNT) {
            this.microPhoneMsgBody = this.deviceHardwareHandler.getMsgBody(buf.slice(MEDIA_MSG_HEADER_COUNT), 'MICROPHONE');
        }
        const type = this.microPhoneMsgHeader.optType;
        switch(type) {
            case this.microPhoneMsgType.OPT_MIC_SET_PARAM_REQ:
                this.deviceHardwareHandler.handleSetRecordParam(this.microPhoneMsgHeader);
                break;
            case this.microPhoneMsgType.OPT_MIC_START_RECORD_REQ:
                this.deviceHardwareHandler.handleStartRecordReq(this.microPhoneMsgBody);
                break;
            case this.microPhoneMsgType.OPT_MIC_STOP_RECOED_REQ:
                this.deviceHardwareHandler.handleStopRecordReq();
                break;
            default:
                break;
        }
    }

    cameraProcessResp(pkg) {
        if (!pkg) {
            return;
        }

        let buf = new Uint8Array(pkg);
        this.cameraMsgHeader = this.deviceHardwareHandler.getMsgHeader(buf);
        if (buf.length > MEDIA_MSG_HEADER_COUNT) {
            this.cameraMsgBody = this.deviceHardwareHandler.getMsgBody(buf.slice(MEDIA_MSG_HEADER_COUNT), 'CAMERA');
        }
        const type = this.cameraMsgHeader.optType;
        switch(type) {
            case this.cameraMsgType.OPT_CAMERA_GET_PARAM_REQ:
                break;
            case this.cameraMsgType.OPT_CAMERA_SET_PARAM_REQ:
                break;
            case this.cameraMsgType.OPT_CAMERA_GET_INFO_REQ:
                break;
            case this.cameraMsgType.OPT_CAMERA_START_PREVIEW_REQ:
                this.deviceHardwareHandler.handleStartPreviewReq(this.cameraMsgBody, this.cameraMsgHeader);
                break;
            case this.cameraMsgType.OPT_CAMERA_STOP_PREVIEW_REQ:
                this.deviceHardwareHandler.handleStopPreviewReq(this.cameraMsgHeader);
                break;
            case this.cameraMsgType.OPT_CAMERA_IS_SUPPORT_H264_REQ:
                break;
            case this.cameraMsgType.OPT_CAMERA_GET_SIZE_REQ:
                break;
            default:
                break;
        }
    }

    sensorProcessResp(pkg) {
        if (!pkg) {
            return;
        }

        let buf = new Uint8Array(pkg);
        this.sensorMsgHeader = this.deviceHardwareHandler.getMsgHeader(buf);
        const type = this.sensorMsgHeader.optType;
        switch(type) {
            case this.sensorMsgType.OPT_SENSOR_ENABLE_RSP:
                // 开启传感器监听并获取数据
                this.util.bind(window, 'devicemotion', this.deviceHardwareHandler.getDeviceMotion.bind(this));
                this.util.bind(window, 'deviceorientation', this.deviceHardwareHandler.getDeviceOrientation.bind(this));
                break;
            case this.sensorMsgType.OPT_SENSOR_DISABLE_RSP:
                // 取消监听停止发送数据
                this.util.unbind(window, 'devicemotion');
                this.util.unbind(window, 'deviceorientation');
                break;
            default:
                break;
        }
    }

    processCmdControlResp(pkg) {
        if (!pkg) {
            return;
        }

        let buf = new Uint8Array(pkg);
        let text = '';
        buf.forEach(c => {
            text += String.fromCharCode(c);
        });
        let resp = this.params2JSON(text);
        let code = Number(resp.code);
        const codeConfig = PROTOCOL_CONFIG.CMD_RESP_TYPE;
        let needToTellExit = false;
        switch (code) {
            case codeConfig.CONNECT_FAILED:
            case codeConfig.VERIFY_FAILED:
            case codeConfig.START_FAILED:
                Logger.debug('Receive failed response, disconnect');
                this.disconnect();
                break;
            case codeConfig.PLAY_TIMEOUT:
                Logger.debug('Play timeout, disconnect');
                this.disconnect();
                break;
            case codeConfig.TOUCH_TIMEOUT:
                Logger.debug('Touch timeout, disconnect');
                this.disconnect();
                break;
            case codeConfig.PAUSE_TIMEOUT:
                Logger.debug('Pause timeout, disconnect');
                this.disconnect();
                break;
            case codeConfig.MEDIA_CONFIG_SUCCESS:
                Logger.debug('Setting media config success');
                this.updateResolution();
                break;
            case codeConfig.START_SUCCESS:
                // 启用传感器
                if (window.DeviceMotionEvent) {
                    this.deviceHardwareHandler.handleSensorEnable(this.sensorMsgType.TYPE_ACCELEROMETER);
                }
                if (window.DeviceOrientationEvent) {
                    this.deviceHardwareHandler.handleSensorEnable(this.sensorMsgType.TYPE_GYROSCOPE);
                }

                this.startPlay();
                this.recMediaConfig = JSON.parse(resp.msg);
                this.isMSE = this.isMSEMode(this.recMediaConfig.frame_type);
                this.initPlyerAndStart();
                this.updateResolution();

                // 进入全屏模式
                this.triggerFullscreen();
                break;
            case codeConfig.INVALID_OPERATION:
            case codeConfig.RECONNECT_PARAMETER_INVALID:
            case codeConfig.RECONNECT_SERVER_UNREACHABLE:
            case codeConfig.RECONNECT_ENGING_START_ERROR:
                // 重连后未重连成功前接收到无效操作，则认为重连失败。场景为：iphone手机切后台导致断连超时后切回应用重连的场景
                if (this.appState.state === APP_STATE_FROM_CLIENT.reconnecting.state) {
                    Logger.debug('Reconnect faild, disconnect');
                    this.disconnect();
                    needToTellExit = true;
                }

                break;
            case codeConfig.H265_NOT_SUPPORT:
                this.startParams.media_config.frame_type = FRAME_TYPE_MAP.TYPE264;
                this.startCloudPhone();
                break;
            default:
                break;
        }

        const tip = APP_STATE_ERROR_CODE_TIP[resp.code];
        this.appState = {state: Number(resp.code), message: resp.msg, tip};
        this.subscribe.trigger('appStateChange', {...this.appState});
        if (needToTellExit) {
            this.appState = APP_STATE_FROM_CLIENT.exit;
            this.subscribe.trigger('appStateChange', {...this.appState});
        }
    }

    processPhoneControlResp(pkg) {
        if (!pkg) {
            return;
        }

        const buf = new Uint8Array(pkg);
        if (!buf.length) {
            return;
        }

        const code = buf[0];
        const stateConfig = PROTOCOL_CONFIG.PHONE_CONNECTION_STATE[code];
        if (stateConfig) {
            Logger.debug(stateConfig.message);
            this.appState = stateConfig;
            this.subscribe.trigger('appStateChange', {...this.appState});
        }
    }

    processChannelData(pkg) {
        this.subscribe.trigger('cloudAppData', pkg.buffer);
    }

    processKeyboardInput(pkg) {
        if (!this.keyboardInput || pkg.length < PROTOCOL_CONFIG.KEYBOARD_INPUT_HEADER_LENGTH) {
            return;
        }

        const [type, highBytesSize, lowByteSize] = pkg;
        if (type === PROTOCOL_CONFIG.KEYBOARD_INPUT_MSG_TYPE.INPUT_EVENT) {
            const len = (highBytesSize << 8) + lowByteSize;
            const input = len > 0 ? this.util.decodeUTF8(pkg, PROTOCOL_CONFIG.KEYBOARD_INPUT_HEADER_LENGTH,
                PROTOCOL_CONFIG.KEYBOARD_INPUT_HEADER_LENGTH + len) : '';
            this.keyboardInput.start(input);
            this.touchHandler.updateKeyBoardMode('KEYBOARD_INPUT');
        } else if (type === PROTOCOL_CONFIG.KEYBOARD_INPUT_MSG_TYPE.HIDE_KEYBOARD_EVENT) {
            this.keyboardInput.stop();
            this.touchHandler.updateKeyBoardMode('KEYBOARD_MAP');
        }
    }

    recordHeartbeat(time) {
        this.networkInfo.heartBeatSendTimes.length <= this.networkInfo.sendTimesMaxCount && (this.networkInfo.heartBeatSendTimes.push(time));
    }

    tryReconnect(trigger) {
        // client自主断连和CAE断连外认为异常断连，尝试重连
        const tempReconnection = this.reconnection;
        tempReconnection.reconnecting = true;
        tempReconnection.trigger = trigger;
        if (tempReconnection.can && tempReconnection.count < tempReconnection.maxTimes) {
            tempReconnection.count++;
            tempReconnection.timerHander && clearTimeout(tempReconnection.timerHander);
            // 延迟reconnect，但需立即触发appStateChange，以便客户显示loading。
            this.appState = this.socketHasOpenned ? APP_STATE_FROM_CLIENT.reconnecting : APP_STATE_FROM_CLIENT.connecting;
            this.subscribe.trigger('appStateChange', {...this.appState});
            tempReconnection.timerHander = setTimeout(() => {
                this.reconnect();
            }, tempReconnection.timerDelay);
        }
        
        // 若重连，会在重连失败后更新appState；若不重连，则直接更新appState。websocket初始建连失败时会进入error和close回调，此时更新为exit状态并不合适。
        if (tempReconnection.maxTimes === 0 && this.appState.state === APP_STATE_FROM_CLIENT.connected.state) {
            this.appState = APP_STATE_FROM_CLIENT.exit;
            this.subscribe.trigger('appStateChange', {...this.appState});
        }
    }

    onClose(state, trigger) {
        this.wsState = state;
        // 断连后清除心跳缓存，规避“断连前发送心跳，异常无法收到心跳响应，断连后重连，恢复心跳后心跳响应和发送错位导致时延计算错误”的问题
        this.networkInfo.heartBeatSendTimes = [];
        Logger.debug('Websocket close. Triggered by ' + trigger);
        this.tryReconnect(trigger);

        if (this.keyboardInput) {
            this.keyboardInput.blurInput();
        }
    }

    getCheckSum(msgType) {
        return (msgType + ((CAE_STREAM_DELIMITER_MAGICWORD >> 8) & 0xFF) + (CAE_STREAM_DELIMITER_MAGICWORD & 0xFF)) & 0xFF;
    }

    paramsSerialize(params) {
        if (params) {
            let kvs = Object.keys(params).map(key => {
                let value = params[key];
                if (typeof value === 'object') {
                    value = Object.keys(value).map(subKey => [subKey, encodeURIComponent(value[subKey])].join('=')).join(':');
                    return [key, value].join('=');
                }

                return [key, encodeURIComponent(value)].join('=');
            });
            return kvs.join('&');
        }

        return '';
    }

    params2JSON(params = '') {
        let json = {};
        params.split('&').forEach(kv => {
            let [key, val] = kv.split('=');
            json[key] = val;
        });
        return json;
    }

    /**
     * 
     * @param {string} mediaType 媒体类型
     * @param {string} msgType 消息类型
     * @param {array} data 8位无符号整型数组Unit8Array 帧数据
     */
    sendMediaData(mediaType, msgType, data, sensorId = null) {
        let dataBuf = this.makeMediaData(msgType, data, mediaType, sensorId);
        let arrayBuf = this.makeDataMsg(mediaType, dataBuf);
        arrayBuf && this.send(arrayBuf);
    }

    /**
     * 根据协议构造发送给CAE的消息
     * @param {object} typedBuf 8位无符号整型数组Uint8Array 
     * @param {string} msgType 消息类型，值为protocalConfig配置的key
     * @param {number} msgBodyLen 消息内容长度
     */
    setMsgHeader(typedBuf, msgType, msgBodyLen) {
        typedBuf[0] = 90;
        typedBuf[1] = 90;
        typedBuf[2] = this.getCheckSum(PROTOCOL_CONFIG.MSG_TYPE[msgType]);
        typedBuf[3] = PROTOCOL_CONFIG.MSG_TYPE[msgType];
        typedBuf[4] = msgBodyLen >> 24;
        typedBuf[5] = msgBodyLen >> 16;
        typedBuf[6] = msgBodyLen >> 8;
        typedBuf[7] = msgBodyLen;
    }

    /**
     * 
     * @param {string} dataType 媒体类型：CAMERA、MICROPHONE
     * @param {object} data Uint8Array 帧数据
     * @param {string} mediaType 媒体类型
     * @returns 
     */
    makeMediaData(dataType, data, mediaType, sensorId = null) {
        let dataBodyLen = data.byteLength;
        let dataBuf = new Uint8Array(MEDIA_MSG_HEADER_COUNT + dataBodyLen);
        dataBuf.set(new Uint8Array(data), MEDIA_MSG_HEADER_COUNT);
        if (mediaType === 'SENSOR') {
            this.deviceHardwareHandler.makeSensorHeaderBuf(dataBuf, dataType, dataBuf.length, sensorId);
        } else {
            this.deviceHardwareHandler.makeCommonHeaderBuf(dataBuf, dataType, dataBuf.length, mediaType);
        }
        

        return dataBuf;
    }

    /**
     * 根据协议构造发送给CAE的消息
     * @param {string} msgType 消息类型，值为protocalConfig配置的key
     * @param {object} params 待转成消息体的控制参数
     * @return {object} 返回消息ArrayBuffer
     */
    makeActionMsg(msgType, msgCmd, params = {}) {
        const MSG_CMD_MAP = {...PROTOCOL_CONFIG.CMD_TYPE};

        let msgBody = this.paramsSerialize({
            command: MSG_CMD_MAP[msgCmd],
            ...params
        });
        let msgBodyLen = msgBody.length;
        let typedBuf = new Uint8Array(PACKAGE_HEADER_LENGTH + msgBodyLen);
        for (let i = 0; i < msgBodyLen; i++) {
            typedBuf[PACKAGE_HEADER_LENGTH + i] = msgBody.charCodeAt(i);
        }

        this.setMsgHeader(typedBuf, msgType, msgBodyLen);
        return typedBuf.buffer;
    }

    /**
     * 根据协议构造发送给CAE的消息
     * @param {string} msgType 消息类型，值为protocalConfig配置的key
     * @param {object} data 待发送的消息数据，ArrayBuffer类型
     * @return {object} 返回消息ArrayBuffer
     */
    makeDataMsg(msgType, data) {
        let msgBodyLen = data.byteLength;
        let typedBuf = new Uint8Array(PACKAGE_HEADER_LENGTH + msgBodyLen);
        typedBuf.set(new Uint8Array(data), PACKAGE_HEADER_LENGTH);
        this.setMsgHeader(typedBuf, msgType, msgBodyLen);

        return typedBuf.buffer;
    }

    decode(data) {
        this.avc.decode(data);
    }

    verifiedData() {
        const userOptions = this.options;
        return {
            ip: userOptions.phoneIp,
            port: userOptions.phonePort,
            session_id: this.sessionId,
            backgroundTimeout: userOptions.backgroundTimeout,
            available_playtime: userOptions.availablePlayTime,
            user_id: userOptions.userId
        };
    }

    initParams(cipherText, verify, iv) {
        this.startParams = {
            ticket: this.options.ticket,
            session_id: this.sessionId,
            auth_ts: this.options.authTimeStamp,
            verify_data: verify,
            encrypted_data: cipherText,
            aes_iv: iv,
            sdk_version: this.options.sdkVersion,
            protocol_version: 'v2',
            client_type: '3',
            media_config: {
                ...this.options.mediaConfig,
                frame_type: FRAME_TYPE_MAP.TYPE265,
                quality: PROTOCOL_CONFIG[DEFAULT_DEFINITION].quality
            }
        };
        // ios/android接入，切到home后有可能会停止发送心跳，该场景下切home超时不生效，切home超时由断连超时决定。
        if (this.options.isMobile) {
            this.startParams.max_disconnect_duration = this.options.backgroundTimeout;
        }
    }

    start() {
        let crypt = new AESGCMCrypto();
        let iv = crypt.iv();
        let verifiedData = this.verifiedData();
        let encryptedData = {...verifiedData, touch_timeout: this.options.touchTimeout};
        Promise.all([
            crypt.encrypt(JSON.stringify(encryptedData), this.options.aesKey, iv),
            crypt.abstract(Object.values(verifiedData).join(''))
        ]).then(([cipherText, verifyText]) => {
            this.initParams(cipherText, verifyText, iv);
            if (cipherText) {
                this.appState = APP_STATE_FROM_CLIENT.connecting;
                this.subscribe.trigger('appStateChange', {...this.appState});
                this.connect();
            }
        });
    }

    connect() {
        if (!this.options.connectURI) {
            return;
        }

        this.initSocket(this.options.connectURI);
    }

    startPlay() {
        this.playing = true;
    }

    stopPlay() {
        this.playing = false;
    }

    initPlayer() {
        // 根据屏幕宽高确定视频方向
        if (this.isMSE) {
            let videoEle = document.createElement('video');
            videoEle.id = this.videoEleId;
            videoEle.muted = true;
            videoEle.playsinline = true;
            videoEle.autoplay = true;
            videoEle.controls = false;
            this.player = videoEle;
        } else {
            this.avc = new CanvasPlayer(this.options);
            this.player = this.avc.canvas;
        }

        const userOptions = this.options;
        if (userOptions.supportAudio) {
            this.audioPlayer = new AudioPlayer({channels: 2, videoPlayer: this.player, volume: userOptions.volume});
            this.audioPlayer.on('audioStateChange', this.triggerSubscribe.bind(this));
        }

        if (this.isMSE) {
            this.player.style.setProperty('width', '100%', 'important');
            this.player.style.setProperty('height', '100%', 'important');
            // 硬解时video标签会按比例填充视频，避免视频被裁减设置contain显示完整画面
            this.player.style.setProperty('object-fit', 'contain', 'important');
        } else {
            // 解决画布居中问题
            this.player.style.setProperty('margin', '0 auto', 'important');
        }
        // 有些手机是硬解场景，将画面覆盖整个video容器，避免出现黑边
        if (this.isMSE && this.options.isMobile) {
            this.player.style.setProperty('object-fit', 'fill', 'important');
        }
        // 解决真机键盘弹起后，浏览器可视窗变小，video高度很小而导致video停止播放，时延增加的问题。
        this.player.style.setProperty('min-height', '60px', 'important');
        // H5 场景下 video、canvas 的 display 属性默认值为 inline，需要主动设置为 block，以避免底部留边问题
        this.player.style.setProperty('display', 'block', 'important');

        // 用户提供的容器
        const videoContainer = document.getElementById(userOptions.containerId);

        // 创建player的容器
        const playerContainerDom = document.createElement('article');
        this.playerContainerId = `J-${new Date().getTime().toString(36)}`;
        playerContainerDom.id = this.playerContainerId;
        playerContainerDom.style.cssText = `
            height: 100%;
            width: 100%;
        `;

        // 是否支持 shadow dom
        const isSupportShadowDom = typeof document.body.attachShadow === 'function';
        if (videoContainer) {
            videoContainer.style.position = 'relative';
            if (isSupportShadowDom) {
                // 支持 shadow dom
                const appContainerShadowRoot = playerContainerDom.attachShadow({
                    mode: 'open'
                });
                const style = document.createElement('style');
                // 重置shadow dom root元素继承的样式
                style.textContent = `
                    :host {
                        all: initial;
                    }
                `;
                appContainerShadowRoot.appendChild(style);
                appContainerShadowRoot.appendChild(this.player);
            } else {
                playerContainerDom.appendChild(this.player);
            }

            videoContainer.appendChild(playerContainerDom);
        }

        if (this.isMSE) {
            this.jmuxer = new JMuxer({
                node: isSupportShadowDom ? this.player : this.videoEleId,
                mode: 'video',
                flushingTime: 1,
                fps: 60,
                debug: false,
                clearBuffer: true
            });
            this.play = this.__msePlay;
            this.toLastest();
        } else {
            this.play = this.__canvasPlay;
        }
    }

    __msePlay(frame) {
        /*global __IS_DEBUG__*/
        if (__IS_DEBUG__) {
            const traceId = window.delayAnalysis.shiftTraceId('receive');
            window.delayAnalysis.record(['decode', 'start', traceId]);
            window.delayAnalysis.record(['play', 'curTime', 'message', traceId], this.player.currentTime);
            let buffered = 0;
            try {
                buffered = this.player.buffered.end(0);
            } catch (e) {
                buffered = null;
            }

            window.delayAnalysis.record(['play', 'buffered', 'message', traceId], buffered);
        }

        let bufTail = new Uint8Array([0x00, 0x00, 0x01, 0x1D, 0x00, 0x00, 0x01, 0x1E, 0x48, 0x53, 0x50, 0x49, 0x43, 0x45, 0x4E, 0x44]);
        let allDataBuf = new Uint8Array(frame.byteLength + bufTail.byteLength);
        allDataBuf.set(frame);
        allDataBuf.set(bufTail, frame.length);
        this.jmuxer.feed({video: allDataBuf, druation: 0});
        this.jmuxer.feed({video: bufTail, druation: 0});
        this.jmuxer.feed({video: bufTail, druation: 0});
    }

    __canvasPlay(frame) {
        this.decode(frame);
    }

    toLastest() {
        let video = this.player;
        let action = () => {
            if (video.buffered && video.buffered.length && video.buffered.end(0)) {
                video.currentTime = video.buffered.end(0);
            }

            video.removeEventListener('canplaythrough', action);
        };

        video.addEventListener('canplaythrough', action);
    }

    disconnect(callbackFn) {
        // client自动断连场景不重连
        this.reconnection.can = false;
        this.stopCloudPhone();
        this.stopHeartbeat();
        // 延迟关闭socket，避免CAE在关闭前接收不到stop cmd
        setTimeout(() => {
            this.closeSocket();
            if (callbackFn) {
                callbackFn();
            }
        }, 1000);
    }

    /*
     * 尝试重连有两种场景：1.首次连接，连接失败；2、已有连接断开后重连，重连失败后再重连
     * 断线后重连，15s内重连，15s后CAE清理资源
    */
    reconnect() {
        this._action = this.socketHasOpenned ? 'reconnect' : 'connect';
        this.connect();
    }

    _reconnect() {
        let arrayBuf = this.makeActionMsg('CMD_CONTROL', 'RECONNECT', {
            session_id: this.sessionId
        });
        arrayBuf && this.send(arrayBuf);
    }

    // 发送启动命令获取音视频数据，启动样例
    startCloudPhone() {
        let arrayBuf = this.makeActionMsg('CMD_CONTROL', 'START', this.startParams);
        arrayBuf && this.send(arrayBuf);
    }

    // 停止样例
    stopCloudPhone() {
        const arrayBuf = this.makeActionMsg('CMD_CONTROL', 'STOP');
        arrayBuf && this.send(arrayBuf);
    }

    pauseDeviceHardware () {
        this.deviceHardwareHandler.pauseDevice();
    }

    resumeDeviceHardware() {
        this.deviceHardwareHandler.resumeDevice();
    }

    // 暂停样例
    // 用于home切换场景，暂停后，CAE停止发送数据
    pauseCloudPhone() {
        const arrayBuf = this.makeActionMsg('CMD_CONTROL', 'PAUSE');
        arrayBuf && this.send(arrayBuf);
    }

    // 恢复样例
    // 切换至home，暂停；60s内切回则发送恢复指令恢复，60s后切回，CAE清理资源，需要重新申请云手机
    resumeCloudPhone() {
        this.frameParser.clearPackageCache('Video');
        if (this.isMSE && this.player.buffered && this.player.buffered.length && this.player.buffered.end(0)) {
            this.player.currentTime = this.player.buffered.end(0);
        }

        this._resumeCloudPhone();
    }

    _resumeCloudPhone() {
        let arrayBuf = this.makeActionMsg('CMD_CONTROL', 'RESUME', {
            session_id: this.sessionId
        });
        arrayBuf && this.send(arrayBuf);
    }

    // 设置画质
    setResolution(clarityVal) {
        const config = PROTOCOL_CONFIG[clarityVal];
        if (this.startParams.media_config && this.startParams.media_config.bitrate) {
            config.bitrate = this.startParams.media_config.bitrate;
        }

        this.setMediaConfig(config);
    }

    // 设置音视频参数
    setMediaConfig(config = {}) {
        let arrayBuf = this.makeActionMsg('CMD_CONTROL', 'SET_MEDIA_CONFIG', {
            media_config: config
        });
        arrayBuf && this.send(arrayBuf);
        if (config.virtual_width && config.virtual_height) {
            this.nextResolution = {
                width: config.virtual_width,
                height: config.virtual_height
            };
        }
    }

    recoverAfterWorkerDead(trigger) {
        this.socketWorker && this.socketWorker.terminate();
        this.createSocket();
        this.onClose(WEBSOCKET_READY_STATE.CLOSED, trigger);
    }

    checkAndRecoverSocket(trigger) {
        
        /* 
        * iOS Safari切home后再回到手机界面，有可能出现socket worker无反应情况(socket已断开，却没有接收到close事件)
        * 主动给socket worker发送信息，200ms内没有从socket worker接收到响应或socket已经close，且未触发close事件（重连机制在close事件处理中触发），主动触发重连
        * 认为200ms内足以触发close事件及接收到从socket worket的响应
        */
        this.checkSocketWorkerState();
        setTimeout(() => {
            if (!this.reconnection.reconnecting && !this.isSocketAvailable()) {
                this.recoverAfterWorkerDead(trigger);
            }
        }, 200);
    }

    listenPageVisibility() {
        let hidden;
        if (typeof document.hidden !== 'undefined') {
            hidden = 'hidden';
            this.visibilityChange = 'visibilitychange';
        } else if (typeof document.msHidden !== 'undefined') {
            hidden = 'msHidden';
            this.visibilityChange = 'msvisibilitychange';
        } else if (typeof document.webkitHidden !== 'undefined') {
            hidden = 'webkitHidden';
            this.visibilityChange = 'webkitvisibilitychange';
        }

        this.handleVisibilityChange = () => {
            if (document[hidden]) {
                // 页面隐藏后停止发送心跳
                Logger.debug('Page hidden, pause');
                // 页面隐藏后虚拟设备停止发送数据
                this.pauseDeviceHardware();
                this.pauseCloudPhone();
            } else if (this.wsState === WEBSOCKET_READY_STATE.OPEN) {
                Logger.debug('Page visibility, resume');
                // 恢复虚拟设备发送数据
                this.resumeDeviceHardware();
                this.resumeCloudPhone();
                this.checkAndRecoverSocket('visibilityChange');
            }
        };

        // 判断浏览器的支持情况
        if (typeof document[hidden] !== 'undefined') {
            this.util.bind(document, this.visibilityChange, this.handleVisibilityChange);
        }
    }

    // 主要处理当前页打开其他页面后再次返回手机页面，从缓存加载，需重连的场景
    listenPageShow() {
        let isPageHide = false;
        window.addEventListener('pageshow', () => {
            if (isPageHide) {
                isPageHide = false;
                this.checkAndRecoverSocket('pageshow');
            }
        });
        window.addEventListener('pagehide', () => {
            isPageHide = true;
        });
    }

    static isSupport() {
        let isSptAudioCxt = window.AudioContext || window.webkitAudioContext;
        let isSptSocket = window.WebSocket;
        return Boolean(AESGCMCrypto.isSupport() && isSptAudioCxt && isSptSocket);
    }

    static isSupportURL() {
        let url = window.URL || window.webkitURL;
        return Boolean(url.createObjectURL);
    }

    on(eventName, callback) {
        this.subscribe.on(eventName, callback);
    }

    off(eventName, callback) {
        this.subscribe.off(eventName, callback);
    }

    generateGUID() {
        return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, c => {
            const r = AESGCMCrypto.getRandomValue() * 16 | 0;
            const v = c === 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }

    isCanvasPriority() {
        // UA 判断
        const userAgent = navigator && navigator.userAgent || '';
        const isMatchUA = Object.values(CANVAS_WHITE_LIST).some(condition => userAgent.includes(condition));

        // oppo 浏览器 UA 特征不明显，通过 window 对象属性进行判断
        const isOppoBrowser = Boolean(window.OppoWebPage || window.OppoFlow || window.oppoErrorPage || window.oppoUrlQuery);
        return isMatchUA || isOppoBrowser;
    }

    isMSEMode(frameType = FRAME_TYPE_MAP.TYPE265) {
        // 使用 MSE 前提：
        // (1) 支持 MediaSource
        // (2) 支持 avc1.42c020 编码
        // (3) 不在 canvas 白名单
        // 前两条判断包含在 JMuxer.isSupported 方法中
        let isMSE = true;
        if (frameType === FRAME_TYPE_MAP.TYPE264) {
            isMSE = Boolean(JMuxer.isSupported('video/mp4; codecs="avc1.42c020"')) && !this.isCanvasPriority();
        } else {
            isMSE = Boolean(JMuxer.isSupported('video/mp4; codecs="hev1"')) && !this.isCanvasPriority();
        }
        
        /*global __IS_DEBUG__*/
        if (__IS_DEBUG__) {
            const framework = this.util.getUrlSearchVal('framework');
            // framework 无值时不进行处理，避免默认值影响 canvas 白名单效果
            if (framework) {
                isMSE = framework.toLowerCase() !== 'canvas';
            }
        }

        return isMSE;
    }

    sendDataToCloudApp(data) {
        if (!this.playing) {
            return;
        }

        let arrayBuf = this.makeDataMsg('CHANNEL', data);
        this.send(arrayBuf)
    }

    exit() {
        this.disconnect(this.terminateSocketWorker.bind(this));
        this.destroy(true);
    }

    terminateSocketWorker() {
        this.socketWorker && this.socketWorker.terminate();
        this.socketWorker = null;
        this.socket = null;
    }

    updateSocketWorkerState(workerState, socketState) {
        this.socketWorkerState.worker = workerState;
        this.socketWorkerState.socket = socketState;
    }

    isSocketAvailable() {
        return this.socketWorkerState.worker === WORKER_STATE.LIVE
            && this.socketWorkerState.socket !== WEBSOCKET_READY_STATE.CLOSING
            && this.socketWorkerState.socket !== WEBSOCKET_READY_STATE.CLOSED;
    }

    // fullscreenElementId: 全屏元素ID。不能在同一个dom上旋转和全屏，所以若自动旋转则需另提供全屏的dom id，如containerId的父级dom
    fullscreenToggle(fullscreenElementId = 'fullscreen-container') {
        this.fullscreen.fullscreenToggle(fullscreenElementId, this.playerContainerId, this.isMSE);
    }

    isFullscreen() {
        return this.fullscreen.isFullscreen();
    }

    triggerFullscreen() {
        const fullscreenBtn = document.getElementById('toggleFullscreen');
        if (fullscreenBtn) {
            fullscreenBtn.addEventListener('click', () => {
                this.fullscreenToggle();
            });
        }
    }

    /**
     * 销毁
     * @param {boolean}} reserveSocketWorker 是否需要销毁socket，exit场景，延迟close socket，close前需保留socket
     * @return {void}}
     */
    destroy(reserveSocketWorker) {
        this.stopPlay();
        this.jmuxer && this.jmuxer.destroy();
        this.avc && this.avc.destroy();
        this.audioPlayer && this.audioPlayer.destroy();
        this.touchHandler && this.touchHandler.destroy();
        this.autoRotation && this.autoRotation.destroy();
        this.keyboardInput && this.keyboardInput.destroy();
        this.fullscreen && this.fullscreen.destroy();
        // 停止传感器
        this.deviceHardwareHandler && this.deviceHardwareHandler.handleSensorDisable(this.sensorMsgType.TYPE_ACCELEROMETER);
        this.deviceHardwareHandler && this.deviceHardwareHandler.handleSensorDisable(this.sensorMsgType.TYPE_GYROSCOPE);
        this.jmuxer = null;
        this.avc = null;
        this.audioPlayer = null;
        this.touchHandler = null;
        this.autoRotation = null;
        this.keyboardInput = null;
        this.fullscreen = null;
        this.deviceHardwareHandler = null;

        if (this.player && this.player.parentNode) {
            if (this.videoResizeCallback) {
                this.player.removeEventListener('resize', this.videoResizeCallback);
            }

            if (this.canvasObserver) {
                this.canvasObserver.disconnect();
                this.canvasObserver = null;
            }

            document.querySelector(`#${this.playerContainerId}`).remove();
            this.playerContainerId = undefined;
            this.player = null;
        }

        if (!reserveSocketWorker) {
            this.terminateSocketWorker();
        }

        this.util.unbind(null, this.visibilityChange);
    }
}

export default AppController;
